'use client'; import { useState, useEffect } from 'react'; import { ChannelDetail, ChannelStatusUpdate } from '@/types/channel'; import { useSignalRContext } from '@/contexts/signalrProvider'; import { buildChannelUrl, formatHandle } from '@/lib/utils/channel'; import YouTubeChatIframe from './YouTubeChatIframe'; import ShareMenu from './ShareMenu'; import FollowButton from '@/app/component/FollowButton'; import Link from 'next/link'; import './style.scss'; // [DEPRECATED] antooza SignalR 채팅 — YouTube Live Chat iframe 으로 대체. // quota 승인 후 재활성화 예정. 자세한 작업 목록: memory/plan_dpot_chat_reactivate_after_quota.md // import ChatSidebar from './ChatSidebar'; type Props = { channel: ChannelDetail; }; function formatCount(n: number): string { if (n >= 10000) { const v = (n / 10000).toFixed(n >= 100000 ? 0 : 1); return `${v.replace(/\.0$/, '')}만명`; } return `${n.toLocaleString()}명`; } export default function WatchView({ channel }: Props) { const [descOpen, setDescOpen] = useState(false); const [isLive, setIsLive] = useState(channel.isLive); const [videoId, setVideoId] = useState(channel.videoId); const [viewerCount, setViewerCount] = useState(channel.viewerCount); const [origin, setOrigin] = useState(null); // ChannelStatusBroadcaster 는 AppHub 의 channel:{sid} 그룹으로 송출하므로 appConnection 사용 필수. // (chatConnection 으로 받으면 그룹 키 미스매치 + ChatHub.JoinChannel 의 입장 메시지 도배 부작용) const { appConnection } = useSignalRContext(); // SSR 시점엔 window 가 없으므로 마운트 후 origin 해석 useEffect(() => { if (typeof window !== 'undefined') { setOrigin(window.location.origin); } }, []); // SignalR 실시간 채널 상태 업데이트 (AppHub.ReceiveChannelStatus — 라이브 시작/종료, 시청자 수) useEffect(() => { if (!appConnection) { return; } const handler = (status: ChannelStatusUpdate) => { if (status.channelSID !== channel.channelSID) { return; } setIsLive(status.isLive); setVideoId(status.videoId); setViewerCount(status.viewerCount); }; appConnection.on('ReceiveChannelStatus', handler); return () => { appConnection.off('ReceiveChannelStatus', handler); }; }, [appConnection, channel.channelSID]); // ChannelStatusBroadcaster 가 AppHub 의 Clients.Group("channel:{sid}") 으로 송출 → AppHub.JoinChannel 로 가입. // AppHub.JoinChannel 은 순수 그룹 가입만 수행 (사이드이펙트 없음). // 재연결 시에도 자동 재가입. useEffect(() => { if (!appConnection) { return; } const sid = channel.channelSID; const join = async () => { if (appConnection.state !== 'Connected') { return; } try { await appConnection.invoke('JoinChannel', sid); } catch (err) { console.warn('[WatchView] JoinChannel 실패:', sid, err); } }; join(); appConnection.onreconnected(join); return () => { if (appConnection.state !== 'Connected') { return; } appConnection.invoke('LeaveChannel', sid).catch(() => {}); }; }, [appConnection, channel.channelSID]); // YouTube embed URL. // - youtube-nocookie.com: 임베드 제한이 youtube.com 대비 관대 (privacy-enhanced mode) // - origin / widget_referrer: 호스트 명시 → "다른 웹사이트에서 재생 차단" 케이스 일부 회피 // - enablejsapi=1: origin 인식에 필요 // - playsinline=1: iOS Safari 인라인 재생 // 채널 소유자가 YouTube Studio에서 "임베드 허용"을 끈 경우는 클라이언트로 해결 불가. const embedUrl = videoId ? `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&playsinline=1&enablejsapi=1${origin ? `&origin=${encodeURIComponent(origin)}&widget_referrer=${encodeURIComponent(origin)}` : ''}` : null; const shareTitle = channel.liveTitle || channel.name; const showViewers = isLive && viewerCount > 0; return (
{/* 플레이어 */}
{embedUrl ? (